/**
 * Copyright 2010 The PlayN Authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package playn.sample.cute.core;

import static playn.core.PlayN.*;

import static java.lang.Math.max;

import playn.core.Image;
import playn.core.Json;
import playn.core.Surface;

import java.util.ArrayList;
import java.util.List;

public class CuteWorld {

  static class Stack {
    int[] tiles;
    List<CuteObject> objects = new ArrayList<CuteObject>();

    int height() {
      return tiles.length;
    }
  }

  private static final String[] tileNames = new String[] { "block_brown",
      "block_dirt", "block_grass", "block_plain", "block_stone", "block_wall",
      "block_water", "block_wood",

      "ramp_north", "ramp_east", "ramp_south", "ramp_west",

      "roof_north", "roof_northeast", "roof_east", "roof_southeast",
      "roof_south", "roof_southwest", "roof_west", "roof_northwest", };

  private static final String[] shadowNames = new String[] { "shadow_east",
      "shadow_northeast", "shadow_north", "shadow_northwest", "shadow_west",
      "shadow_southwest", "shadow_south", "shadow_southeast",
      "shadow_side_west" };

  private static final int SHADOW_EAST = 0;
  private static final int SHADOW_NORTHEAST = 1;
  private static final int SHADOW_NORTH = 2;
  private static final int SHADOW_NORTHWEST = 3;
  private static final int SHADOW_WEST = 4;
  private static final int SHADOW_SOUTHWEST = 5;
  private static final int SHADOW_SOUTH = 6;
  private static final int SHADOW_SOUTHEAST = 7;
  private static final int SHADOW_SIDE_WEST = 8;

  private static final int TILE_WIDTH = 100;
  private static final int TILE_HEIGHT = 80;
  private static final int TILE_DEPTH = 40;
  private static final int TILE_BASE = 90;
  private static final int TILE_IMAGE_HEIGHT = 170;
  private static final int OBJECT_BASE = 30;

  private static final double GRAVITY = -10.0;
  private static final double RESTITUTION = 0.4;
  private static final double FRICTION = 10.0;

  private static final int MAX_STACK_HEIGHT = 8;
  private static final Stack EMPTY_STACK;

  static {
    EMPTY_STACK = new Stack();
    EMPTY_STACK.tiles = new int[0];
  }

  private final Image[] tiles = new Image[tileNames.length];
  private final Image[] shadows = new Image[shadowNames.length];

  private Stack[] world;
  private int worldWidth, worldHeight;
  private double viewOriginX, viewOriginY, viewOriginZ;
  private int updateCounter = -1;

  public CuteWorld(Json.Object data) {
    loadImages();
    initWorld(data);
  }

  public CuteWorld(int width, int height) {
    worldWidth = width;
    worldHeight = height;

    loadImages();

    this.world = new Stack[worldWidth * worldHeight];
    int i = 0;
    for (int ty = 0; ty < worldHeight; ++ty) {
      for (int tx = 0; tx < worldWidth; ++tx) {
        this.world[i] = new Stack();
        world[i].tiles = new int[0];
        ++i;
      }
    }
  }

  public void addObject(CuteObject o) {
    Stack stack = stackForObject(o);
    stack.objects.add(o);
    o.stack = stack;
  }

  public void addTile(int tx, int ty, int type) {
    Stack stack = stack(tx, ty);
    int len = stack.tiles.length;
    if (len == MAX_STACK_HEIGHT) {
      return;
    }

    int[] newTiles = new int[len + 1];
    System.arraycopy(stack.tiles, 0, newTiles, 0, len);
    stack.tiles = newTiles;
    stack.tiles[len] = type;
  }

  public void removeTopTile(int tx, int ty) {
    Stack stack = stack(tx, ty);
    int len = stack.tiles.length;
    if (len == 0) {
      return;
    }

    int[] newTiles = new int[len - 1];
    System.arraycopy(stack.tiles, 0, newTiles, 0, len - 1);
    stack.tiles = newTiles;
  }

  public void paint(Surface surf, float alpha) {
    int startX = (int) pixelToWorldX(surf, 0);
    int endX = (int) pixelToWorldX(surf, surf.width());
    if (startX < 0)
      startX = 0;
    if (endX < 0)
      endX = 0;
    if (startX >= worldWidth)
      startX = worldWidth - 1;
    if (endX >= worldWidth)
      endX = worldWidth - 1;

    int startY = (int) pixelToWorldY(surf, 0, 0);
    int endY = (int) pixelToWorldY(surf, surf.height(), MAX_STACK_HEIGHT);
    if (startY < 0)
      startY = 0;
    if (endY < 0)
      endY = 0;
    if (startY >= worldHeight)
      startY = worldHeight - 1;
    if (endY >= worldHeight)
      endY = worldHeight - 1;

    // Paint all the tiles from back to front.
    for (int tz = 0; tz < MAX_STACK_HEIGHT; ++tz) {
      for (int ty = startY; ty <= endY; ++ty) {
        for (int tx = startX; tx <= endX; ++tx) {
          Stack stack = world[ty * worldWidth + tx];

          if (tz < stack.height()) {
            // Draw the tile and its shadows.

            // Skip obviously hidden tiles.
            if ((tz < stack.height() - 1) && (height(tx, ty + 1) > tz)) {
              continue;
            }

            // Figure out where the tile goes. If it's out of screen bounds,
            // skip it (paintShadow() is relatively expensive).
            int px = worldToPixelX(surf, tx);
            int py = worldToPixelY(surf, ty, tz) - TILE_BASE;
            if ((px > surf.width()) || (py > surf.height())
                || (px + TILE_WIDTH < 0) || (py + TILE_IMAGE_HEIGHT < 0)) {
              continue;
            }

            surf.drawImage(tiles[stack.tiles[tz]], px, py);
            paintShadow(surf, tx, ty, px, py);
          } else if (tz >= stack.height()) {
            // Paint the objects in this stack.
            paintObjects(surf, stack, tz, alpha);
          }
        }
      }
    }
  }

  public void setViewOrigin(double x, double y, double z) {
    viewOriginX = x;
    viewOriginY = y;
    viewOriginZ = z;
  }

  public void updatePhysics(double delta) {
    for (int ty = 0; ty < worldHeight; ++ty) {
      for (int tx = 0; tx < worldWidth; ++tx) {
        updatePhysics(stack(tx, ty), delta);
      }
    }
    updatePhysics(EMPTY_STACK, delta);

    ++updateCounter;
  }

  public void write(Json.Writer w) {
    w.object();
    {
      w.value("width", worldWidth);
      w.value("height", worldHeight);

      w.array("stacks");
      for (int y = 0; y < worldHeight; ++y) {
        for (int x = 0; x < worldWidth; ++x) {
          Stack stack = stack(x, y);
          w.array();
          for (int z = 0; z < stack.height(); ++z) {
            w.value(stack.tiles[z]);
          }
          w.end();
        }
      }
      w.end();
    }
    w.end();
  }

  private int height(int tx, int ty) {
    return stack(tx, ty).height();
  }

  private String imageRes(String name) {
    return "images/" + name + ".png";
  }

  private void initWorld(Json.Object data) {
    worldWidth = data.getInt("width");
    worldHeight = data.getInt("height");

    this.world = new Stack[worldWidth * worldHeight];

    Json.Array stacksData = data.getArray("stacks");
    int i = 0;
    for (int ty = 0; ty < worldHeight; ++ty) {
      for (int tx = 0; tx < worldWidth; ++tx) {
        Json.Array stackData = stacksData.getArray(i);
        world[i] = new Stack();
        world[i].tiles = new int[stackData.length()];
        for (int tz = 0; tz < stackData.length(); ++tz) {
          world[i].tiles[tz] = stackData.getInt(tz);
        }
        ++i;
      }
    }

    viewOriginX = 0;
    viewOriginY = 2.5;
  }

  private void loadImages() {
    // Load tiles.
    for (int i = 0; i < tiles.length; ++i) {
      tiles[i] = assets().getImage(imageRes(tileNames[i]));
    }

    // Load shadows.
    for (int i = 0; i < shadows.length; ++i) {
      shadows[i] = assets().getImage(imageRes(shadowNames[i]));
    }
  }

  /**
   * Moves an object by the given vector, handling all collisions.
   */
  private void moveBy(CuteObject o, double dx, double dy, double dz) {
    // Walls - start by getting relative heights of neighbors
    int tx = (int) o.x, ty = (int) o.y;
    int hc = (int) o.z;
    int hn = height(tx, ty - 1);
    int hs = height(tx, ty + 1);
    int hw = height(tx - 1, ty);
    int he = height(tx + 1, ty);
    int hse = height(tx + 1, ty + 1);
    int hne = height(tx + 1, ty - 1);
    int hsw = height(tx - 1, ty + 1);
    int hnw = height(tx - 1, ty - 1);

    double left = o.x + dx - o.r, right = o.x + dx + o.r;
    double top = o.y + dy - o.r, bottom = o.y + dy + o.r;
    boolean pastLeft = left < tx, pastTop = top < ty;
    boolean pastRight = right > tx + 1, pastBottom = bottom > ty + 1;

    // Collisions: north, east, west, south.
    if (pastLeft) {
      if (hw > hc) {
        dx = tx + o.r - o.x;
        o.vx = -o.vx * RESTITUTION;
      }
    } else if (pastRight) {
      if (he > hc) {
        dx = tx + 1 - o.r - o.x;
        o.vx = -o.vx * RESTITUTION;
      }
    }

    if (pastTop) {
      if (hn > hc) {
        dy = ty + o.r - o.y;
        o.vy = -o.vy * RESTITUTION;
      }
    } else if (pastBottom) {
      if (hs > hc) {
        dy = ty + 1 - o.r - o.y;
        o.vy = -o.vy * RESTITUTION;
      }
    }

    // Collisions: nw, ne, se, sw.
    if (pastLeft && pastTop) {
      if (hnw > hc) {
        if (tx - left > ty - top) {
          dy = ty - (o.y - o.r);
          o.vy = -o.vy * RESTITUTION;
        } else {
          dx = tx - (o.x - o.r);
          o.vx = -o.vx * RESTITUTION;
        }
      }
    }

    if (pastRight && pastTop) {
      if (hne > hc) {
        if (right - (tx + 1) > ty - top) {
          dy = ty - (o.y - o.r);
          o.vy = -o.vy * RESTITUTION;
        } else {
          dx = (tx + 1) - (o.r + o.x);
          o.vx = -o.vx * RESTITUTION;
        }
      }
    }

    if (pastRight && pastBottom) {
      if (hse > hc) {
        if (right - (tx + 1) > bottom - (ty + 1)) {
          dy = (ty + 1) - (o.r + o.y);
          o.vy = -o.vy * RESTITUTION;
        } else {
          dx = (tx + 1) - (o.r + o.x);
          o.vx = -o.vx * RESTITUTION;
        }
      }
    }

    if (pastLeft && pastBottom) {
      if (hsw > hc) {
        if (tx - left > bottom - (ty + 1)) {
          dy = (ty + 1) - (o.r + o.y);
          o.vy = -o.vy * RESTITUTION;
        } else {
          dx = tx - (o.x - o.r);
          o.vx = -o.vx * RESTITUTION;
        }
      }
    }

    // Update x/y position.
    o.x = o.x + dx;
    o.y = o.y + dy;

    // Clamp to world bounds.
    if (o.x < o.r) {
      o.x = o.r;
    }
    if (o.y < o.r) {
      o.y = o.r;
    }
    if (o.x > worldWidth - o.r) {
      o.x = worldWidth - o.r;
    }
    if (o.y > worldHeight - o.r) {
      o.y = worldHeight - o.r;
    }

    // Collisions: floors.
    left = o.x + dx - o.r;
    right = o.x + dx + o.r;
    top = o.y + dy - o.r;
    bottom = o.y + dy + o.r;
    pastLeft = left < tx - 0.01;
    pastTop = top < ty - 0.01;
    pastRight = right > tx + 1.01;
    pastBottom = bottom > ty + 1.01;

    double floor = height(tx, ty);
    if (pastLeft && hw - o.z < 0.5) {
      floor = max(floor, hw);
    }
    if (pastTop && hn - o.z < 0.5) {
      floor = max(floor, hn);
    }
    if (pastRight && he - o.z < 0.5) {
      floor = max(floor, he);
    }
    if (pastBottom && hs - o.z < 0.5) {
      floor = max(floor, hs);
    }

    if (o.z + dz < floor) {
      dz = floor - o.z;
      o.vz = -o.vz * RESTITUTION;

      if (o.vz < 0.01) {
        o.vz = 0;
      }
      o.resting = true;
    } else {
      o.resting = o.vz == 0;
    }

    o.z = o.z + dz;

    // Clamp to world bounds.
    if (o.z < 0) {
      o.z = 0;
    }
    if (o.z > MAX_STACK_HEIGHT - 0.01) {
      o.z = MAX_STACK_HEIGHT - 0.01;
    }
  }

  private void paintObjects(Surface surf, Stack stack, int tz, float alpha) {
    for (CuteObject o : stack.objects) {
      if ((int) o.z == tz) {
        int px = worldToPixelX(surf, o.x(alpha));
        int py = worldToPixelY(surf, o.y(alpha), o.z(alpha));
        float baseX = o.img.width() / 2;
        float baseY = o.img.height() - OBJECT_BASE;
        surf.drawImage(o.img, px - baseX, py - baseY);
      }
    }
  }

  private void paintShadow(Surface surf, int tx, int ty, int px, int py) {
    int hc = height(tx, ty);
    int hn = height(tx, ty - 1);
    int hs = height(tx, ty + 1);
    int hw = height(tx - 1, ty);
    int he = height(tx + 1, ty);
    int hse = height(tx + 1, ty + 1);
    int hne = height(tx + 1, ty - 1);
    int hsw = height(tx - 1, ty + 1);
    int hnw = height(tx - 1, ty - 1);

    if (hn > hc) {
      surf.drawImage(shadows[SHADOW_NORTH], px, py);
    }
    if (hs > hc) {
      surf.drawImage(shadows[SHADOW_SOUTH], px, py);
    }
    if (he > hc) {
      surf.drawImage(shadows[SHADOW_EAST], px, py);
    }
    if (hw > hc) {
      surf.drawImage(shadows[SHADOW_WEST], px, py);
    }

    if ((hse > hc) && (he <= hc)) {
      surf.drawImage(shadows[SHADOW_SOUTHEAST], px, py);
    }
    if ((hsw > hc) && (hw <= hc)) {
      surf.drawImage(shadows[SHADOW_SOUTHWEST], px, py);
    }
    if ((hne > hc) && (he <= hc) && (hn <= hc)) {
      surf.drawImage(shadows[SHADOW_NORTHEAST], px, py);
    }
    if ((hnw > hc) && (hw <= hc) && (hn <= hc)) {
      surf.drawImage(shadows[SHADOW_NORTHWEST], px, py);
    }

    // Special case: the side shadow has to be drawn potentially multiple
    // times, because it's rendered on the front face of the stack.
    while (hc > 0) {
      if ((hsw >= hc) && (hs < hc)) {
        surf.drawImage(shadows[SHADOW_SIDE_WEST], px, py);
      }
      py += TILE_DEPTH;
      if (hs >= hc) {
        break;
      }
      --hc;
    }
  }

  private double pixelToWorldX(Surface surf, float x) {
    double center = surf.width() * 0.5;
    return (int) (((viewOriginX * TILE_WIDTH) + x - center) / TILE_WIDTH);
  }

  private double pixelToWorldY(Surface surf, float y, double z) {
    double center = surf.height() * 0.5;
    return (y + (viewOriginY * TILE_HEIGHT - viewOriginZ * TILE_DEPTH)
        + (z * TILE_DEPTH) - center)
        / TILE_HEIGHT;
  }

  private Stack stack(int tx, int ty) {
    if ((tx < 0) || (tx >= worldWidth) || (ty < 0) || (ty >= worldHeight)) {
      return EMPTY_STACK;
    }

    return world[ty * worldWidth + tx];
  }

  private Stack stackForObject(CuteObject o) {
    if ((o.x < 0) || (o.y < 0) || (o.x >= worldWidth) || (o.y >= worldHeight)) {
      return EMPTY_STACK;
    }

    return stack((int) o.x, (int) o.y);
  }

  private void updatePhysics(CuteObject o, double delta) {
    // Avoid double-updates.
    if (o.lastUpdated == updateCounter) {
      return;
    }
    o.lastUpdated = updateCounter;
    o.saveOldPos();

    // Gravity & friction.
    if (o.z > o.stack.height()) {
      o.az += delta * GRAVITY;
    }

    if (o.resting) {
      o.vx -= o.vx * FRICTION * delta;
      o.vy -= o.vy * FRICTION * delta;
      if (o.vz < 0) {
        o.vz = 0;
      }
    }

    // Update velocity
    o.vx += o.ax * delta;
    o.vy += o.ay * delta;
    o.vz += o.az * delta;

    // Update position and handle collisions.
    moveBy(o, o.vx, o.vy, o.vz);
  }

  private void updatePhysics(Stack stack, double delta) {
    for (int i = 0; i < stack.objects.size(); ++i) {
      // Run physics.
      CuteObject o = stack.objects.get(i);
      updatePhysics(o, delta);

      // Re-sort.
      Stack newStack = stackForObject(o);
      if (stack != newStack) {
        stack.objects.remove(i--);
        newStack.objects.add(o);
        o.stack = newStack;
      }
    }
  }

  private int worldToPixelX(Surface surf, double x) {
    double center = surf.width() * 0.5;
    return (int) (center - (viewOriginX * TILE_WIDTH) + x * TILE_WIDTH);
  }

  private int worldToPixelY(Surface surf, double y, double z) {
    double center = surf.height() * 0.5;
    return (int) (center
        - (viewOriginY * TILE_HEIGHT - viewOriginZ * TILE_DEPTH) + y
        * TILE_HEIGHT - z * TILE_DEPTH);
  }

  public double worldWidth() {
    return worldWidth;
  }

  public double worldHeight() {
    return worldHeight;
  }
}
